אחרי שדיברנו על קרון ודמונים שמאפשרים לנו להריץ קוד גם בלי גולשים, היום נזרז את טעינת העמוד על ידי זה שניפטר כמה שיותר מהר מהגולש שעומד לשרת על הראש.
איך עובד שרת עם PHP
לא אחדש בזה שאומר שהתהליך המוכר לכם הוא — בקשה מהדפדפן מגיעה אל השרת, השרת רואה שהבקשה היא אל קובץ עם סיומת .php ומפעיל את מפענח ה-php. מפענח ה-php קורא את הסקריפט מהקובץ, מבצע את כל הפקודות שבו שורה אחרי שורה ובסיום הסקריפט מחזיר לשרת את התוכן המיוצר. השרת מקבל מהמפענח את התוכן הזה ושולח אותו בחזרה לדפדפן.
לאן נעלם הפלט או קצת על output buffering
כל שורת echo '...'; גורמת ל-php להעביר פלט נוסף אל השרת, כדי שהוא, בתורו, יישלח ללקוח.
אבל לרוב זה לא קורה ובמקום זה php אוגרת בעצמה את כל הפלט עד סיום הסקריפט מכמה סיבות:
- אנשים מסוימים לא מודעים ל output buffering מופעל בתור ברירת מחדל ב-php
- אחרים בכוונה יורים לעצמם ברגל עם ob_start בתור פתרון גרוע לבעיית ה headears already sent
- ומישהו בכלל מפעיל gzip דרך php ולא דרך השרת.
יותר מדי איטי
מבחינתנו כל זה אומר שהפלט יגיע לגולש רק אחרי סיום הסקריפט. ומה אם במקום זה הפלט היה מגיעה לגולש באותו רגע שבו נשלח? מה עם בזמן שאנחנו שולפים מהמסד את תוכן הכתבה - הדפדפן כבר היה מתחיל לצייר את התפריט העליון או לטעון javascriptים ?
נשמע אוטופי מדי? בכל זאת בואו ננסה.
סקריפט לדוגמה:
echo 'line1';
sleep(10); // some work goes around here
echo 'line2';
sleep(10); // some work goes around here
echo 'line2';
רק 10 שניות לאחר ההפעלה יופיעו על המסך שני השורות. במקום שיופיעו אחת וכמה שניות אחרי — השנייה. php החליטה לשמור לעצמה את כל הפלט ולא להעביר אותו הלאה לפני סיום הסקריפט.
flush()
flush() היא הפקודה שאומרת ל-php לזרוק את כל הפלט שהיא אוגרת לשרת. הופסת השורה הזו אחרי הפלט תגרום ל-php להעביר את הפלט לשרת ומשם ללקוח בלי לחכות להמשך הסקריפט.
בדרך כלל רק הפקודה הזו לא מספיקה ופותרת רק את הסיבה הראשונה ברשימה הקודמת. אליה מצטרפת גם הפקודה ob_flush שעושה אותו דבר עבור פלט שנצבר על ידי output_buffering מוגדר ולא על ידי php עצמה שפתורת את הסעיף השני. ואחרי השינויים הקוד החדש:
echo 'line1';
ob_flush(); flush();
sleep(10); // some work goes around here
echo 'line2';
ob_flush(); flush();
sleep(10); // some work goes around here
echo 'line2';
למי שהדוגמה הזאת עדיין לא נתנה שיפור - נשאר לפתור את הסעיף השלישי עם הפקודה הבאה
ini_set('zlib.output_compression', 0);
output buffering ברמת השרת
לאלה שבינינו מפעילים nginx או איפה ש-php מקונפג בתור fast cgi בדרך הכלל השרת ינסה לחכות לסיום הפרוצס של הסקריפט גם כן. במקרה הזה יש לשנות מעט את ההגדרות של השרת עצמו: לכבות gzip ו proxy
לסגור את החיבור מוקדם
אחד השימושים הכי מועילים שלי לכל הסיפור הוא לשלוח תשובה לגולש כמה שיותר מהר ואחרי שנסיים עם הפלט לגולש - לנתק את הבקשה ולהמשיך בפעולות התפעוליות שאפשר להשאיר לסוף. לדוגמה - לשלוח למשתמש את העמוד שביקוד ואחרי זה לרענן את הקאש או לעדכן במסד את תאריך ביקור המשתמש וכמות הצפיות בכתה, לשלוח אימיילים ולמחוק קבצים.
את כל אלה אפשר לעשות אחרי שליחת התשובה למשתמש וסגירת החיבור איתו בצורה הבאה:
<?php
// turn off server side output buffering
@apache_setenv('no-gzip', 1);
// turn off php gzipping
@ini_set('zlib.output_compression', 0);
// send response headers and close the connection
header('Content-type: text/html');
header('Connection: close');
// start output buffering
ob_start();
/********************************************/
/************ YOUR OUTPUT GOES HERE *********/
/********************************************/
echo 'blablabla';
// Send the content-length header
header("Content-Length: ".mb_strlen($response));
// Now flush all of our verbose buffers
ob_end_flush();
@ob_flush();
flush();
/*********************************************/
/********* CODE AFTER USER *******************/
/*********************************************/
// mysql_query(update x)
// mail(y)
// fputs(z)
// turn off server side output buffering
@apache_setenv('no-gzip', 1);
// turn off php gzipping
@ini_set('zlib.output_compression', 0);
// send response headers and close the connection
header('Content-type: text/html');
header('Connection: close');
// start output buffering
ob_start();
/********************************************/
/************ YOUR OUTPUT GOES HERE *********/
/********************************************/
echo 'blablabla';
// Send the content-length header
header("Content-Length: ".mb_strlen($response));
// Now flush all of our verbose buffers
ob_end_flush();
@ob_flush();
flush();
/*********************************************/
/********* CODE AFTER USER *******************/
/*********************************************/
// mysql_query(update x)
// mail(y)
// fputs(z)
מתי להשתמש
אחד החסרונות הוא חוסר האפשרות להשתמש ב-gzip .
המקום המתאים לנצל אפשרות זו הוא בסקריפטים שהפלט שלהם קצר וזמן הפעילות ארוך למשל סקריפטים שעונים לבקשות ajax עם עדכון נתונים או גישה לנתונים מרוחקים.
אגב, גם על זה אפשר להתגבר על ידי מעבר מ gzip ל sdch שלא צריך את כל הפלט לפני שהוא עושה את הקיבוץ ואגב, לפי הדו"ח של אופרה על spdy הוא גם יותר יעיל.
מה לא צריך לעשות?
לא צריך לרוץ לשים אחרי כל echo את flush. במקרה הטוב עדיף לשים שניים בעמוד, אחד אחרי התפריט העליון או החלקים הקבועים של האתר והשני במקום בו מתוכנן להסתיים הפלט ומתחילות פקודות תפעוליות כלליות שלא מייצרות פלט למשתמש.
תגובות לכתבה:
אלכס, כתבת בטעות кок במקום php... לתשומת לבך (:
מדריך מעולה ד"א.
:)
תודה
מדריך מעולה :)
מימוש מאוד טוב לנושא הזה נקרא BIG PIPE ופייסבוק עובדים איתו:
http://www.facebook.com/note.php?note_id=389414033919
זה בעצם דרך לעבוד עם PHP בצורה אסינכרונית
ועוד פונקציה חזקה שיכולה לעזור אבל זמינה רק עם PHP-FPM:
http://il.php.net/manual/en/install.fpm.php
fastcgi_finish_request()
מאמר מעולה!
יש דרך לממש את זה ב-Yii?
כי תאכלס מתי שמגיע הרינדור.. כבר כל הכובד יצא מהשרת..
יש דרך מעניינת ו"חוקית" לעשות את זה?
הקוד:
echo 'line1';
ob_flush(); flush();
sleep(10); // some work goes around here
echo 'line2';
לא עובד לי , שולח את הכל בבת אחת, גם עם שמתי לפני זה
ini_set('zlib.output_compression', 0);
שכחת לציין שOUTPUT BUFFRING מתחיל רק לאחר שהתקבלו לדפדפן 4098 (כרום לדוגמא) בתים לפחות
לכן יש להשתמש בSTR REPEAT על מנת לשלוח מספר תווים שכזה
ולכן קוד פשוט כמו זה לא יעבוד לפני שיתווספו תווים נוספים..
echo 'line1';
ob_flush(); flush();
sleep(10); // some work goes around here
echo 'line2';
חחח אחי עכשיו נזכרת? הבעיה (לפי מה שאני זוכר, זה היה כבר מלפני חצי שנה) הייתה בכלל בהידר שלא שלחתי.
ודרך אגב, זה לא נכון.